package com.cloudlead.common.persistence.impl;

import com.cloudlead.common.lang.BusinessModule;
import com.cloudlead.common.lang.Messages;
import com.cloudlead.common.lang.annotation.Unique;
import com.cloudlead.common.lang.annotation.Uniques;
import com.cloudlead.common.lang.exception.RequestNotAcceptableException;
import com.cloudlead.common.lang.exception.ServerErrorException;
import com.cloudlead.common.lang.persistence.*;
import com.cloudlead.common.lang.persistence.Sort.Direction;
import com.cloudlead.common.lang.persistence.Sort.Order;
import com.cloudlead.common.lang.service.AbstractBaseService;
import com.cloudlead.common.lang.service.CrudService;
import com.cloudlead.common.lang.utils.ObjectId;
import com.cloudlead.common.persistence.CrudRepository;
import com.cloudlead.common.persistence.DynamicSpecifications;
import com.cloudlead.common.persistence.EntityPathResolver;
import com.cloudlead.common.persistence.Specification;
import com.cloudlead.common.persistence.bean.BeanConvertUtils;
import com.cloudlead.common.persistence.bean.BeanDesc;
import com.cloudlead.utils.cache.ListCacheService;
import com.mysema.query.sql.RelationalPath;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.Path;
import com.mysema.query.types.Predicate;
import com.mysema.query.types.path.PathBuilder;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Transactional(readOnly = true)
public abstract class AbstractCrudService<Repository extends CrudRepository<Entity, String>, VO extends IdEntity<String>, Entity extends IdEntity<String>>
        extends AbstractBaseService implements CrudService<VO> {

    protected static final String ID = "id";

    protected final Repository repository;

    protected final EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE;

    protected Class<VO> voClass;

    protected BeanDesc voBeanDesc;

    protected Class<Entity> entityClass;

    protected RelationalPath<Entity> root;

    protected PathBuilder<Entity> builder;

    @Autowired(required = false)
    protected ListCacheService cacheService;

    protected boolean hasCache;

    public AbstractCrudService(BusinessModule module, Repository repository) {
        super(module);
        initType(this.getClass());
        this.repository = repository;
    }

    @SuppressWarnings("unchecked")
    private void initType(Class<?> cls) {
        Type superType = cls.getGenericSuperclass();
        if (superType instanceof ParameterizedType) {
            ParameterizedType ptype = (ParameterizedType) superType;
            Type cmpType = ptype.getActualTypeArguments()[1];
            this.voClass = (Class<VO>) cmpType;
            Type cmpType1 = ptype.getActualTypeArguments()[2];
            this.entityClass = (Class<Entity>) cmpType1;
        }

        BeanConvertUtils.registerVOType(voClass);
        voBeanDesc = BeanConvertUtils.getVOBeanDesc(voClass);

        root = entityPathResolver.createPath(entityClass);
        builder = entityPathResolver.getPathBuilder(root);
    }

    @Override
    @Transactional
    public void create(VO vo) {
        Assert.notNull(vo, "create vo is required.");
        uniqueValidator(vo);
        if (StringUtils.isBlank(vo.getId())) {
            vo.setId(ObjectId.get());
        }
        repository.save(BeanConvertUtils.convertVOToDomain(vo, entityClass));

        if (hasCache && null != cacheService) {
            clearCachedDatas(this.getModule().getFullName());
        }
    }

    @Override
    @Transactional
    public void update(VO vo) {
        Assert.notNull(vo, "update vo is required.");
        uniqueValidator(vo);
        repository.update(BeanConvertUtils.convertVOToDomain(vo, entityClass));

        if (hasCache && null != cacheService) {
            clearCachedDatas(this.getModule().getFullName());
            cacheService.delete(vo.getId());
        }
    }

    @Override
    @Transactional
    public void delete(String id) {
        Assert.notNull(id, "delete id is required.");
        repository.deleteById(id);

        if (hasCache && null != cacheService) {
            clearCachedDatas(this.getModule().getFullName());
            cacheService.delete(id);
        }
    }

    void uniqueValidator(VO vo) {
        List<Unique> uniqueList = getUniques();
        if (!uniqueList.isEmpty()) {
            for (Unique uni : uniqueList) {
                try {
                    String[] propertys = uni.value();
                    if (null != propertys) {
                        boolean isEmpty = true;
                        for (String property : propertys) {
                            if (null != FieldUtils.readDeclaredField(vo,
                                    property, true)) {
                                isEmpty = false;
                            }
                        }
                        if (isEmpty) {
                            continue;
                        }
                    }
                    Filter filter = new Filter();
                    Criteria criteria = new Criteria();
                    filter.or(criteria);
                    if (null != vo.getId()) {
                        criteria.and(new Term(ID, Operator.NE, vo.getId()));
                    }
                    for (String property : propertys) {
                        criteria.and(new Term(property, FieldUtils
                                .readDeclaredField(vo, property, true)));
                    }
                    long count = count(filter);
                    if (0 < count) {
                        throw new RequestNotAcceptableException(Messages.getMessage("persistence.unique.error",
                                voClass.getSimpleName(), StringUtils.join(propertys, ",")));
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    throw new ServerErrorException(
                            Messages.getMessage("persistence.property.read.error"), e);
                }
            }
        }
    }

    private List<Unique> getUniques() {
        List<Unique> uniqueList = new ArrayList<Unique>();
        Uniques uniques = voClass.getAnnotation(Uniques.class);
        if (null != uniques) {
            uniqueList.addAll(Arrays.asList(uniques.values()));
        }
        Unique unique = voClass.getAnnotation(Unique.class);
        if (null != unique) {
            uniqueList.add(unique);
        }
        return uniqueList;
    }

    @Override
    public VO view(String id) {
        Assert.notNull(id, "view id is required.");
        return BeanConvertUtils.convertDomainToVO(repository.findById(id),
                voClass);
    }

    @Override
    public VO findOne(Filter filter) {
        Predicate predicate = null;
        if (null != filter) {
            predicate = convertFilter(entityClass, filter);
        }
        Entity findOne = repository.findOne(predicate);
        return BeanConvertUtils.convertDomainToVO(findOne, voClass);
    }

    @Override
    public VO findOne(VO example) {
        Predicate predicate = null;
        if (null != example) {
            predicate = convertExample(entityClass,
                    BeanConvertUtils.convertVOToDomain(example, entityClass));
        }
        Entity findOne = repository.findOne(predicate);
        return BeanConvertUtils.convertDomainToVO(findOne, voClass);
    }

    @Override
    public List<VO> list(Filter filter, Sort sort) {
        Predicate predicate = null;
        if (null != filter) {
            predicate = convertFilter(entityClass, filter);
        }
        OrderSpecifier<?>[] orders = null;
        // 处理排序
        if (null != sort) {
            orders = new OrderSpecifier<?>[sort.size()];
            for (int i = 0; i < sort.size(); i++) {
                orders[i] = toOrder(root, builder, sort.get(i));
            }
        }

        List<Entity> findAll = repository.findAll(predicate, orders);
        return BeanConvertUtils.convertDomainToVO(findAll, voClass);
    }

    @Override
    public long count(VO example) {

        Predicate predicate = null;
        if (null != example) {
            predicate = convertExample(entityClass,
                    BeanConvertUtils.convertVOToDomain(example, entityClass));
        }
        return repository.count(predicate);
    }

    @Override
    public List<VO> list(VO example, Sort sort) {
        Predicate predicate = null;
        if (null != example) {
            predicate = convertExample(entityClass,
                    BeanConvertUtils.convertVOToDomain(example, entityClass));
        }
        OrderSpecifier<?>[] orders = null;
        // 处理排序
        if (null != sort) {
            orders = new OrderSpecifier<?>[sort.size()];
            for (int i = 0; i < sort.size(); i++) {
                orders[i] = toOrder(root, builder, sort.get(i));
            }
        }
        return BeanConvertUtils.convertDomainToVO(
                repository.findAll(predicate, orders), voClass);
    }

    @Override
    public long count(Filter filter) {
        // 处理查询条件
        Predicate predicate = null;
        if (null != filter) {
            predicate = convertFilter(entityClass, filter);
        }
        return repository.count(predicate);
    }

    @Override
    public Page<VO> findAll(PageRequest pageRequest) {
        Assert.notNull(pageRequest, "pageRequest is required.");
        Filter filter = pageRequest.getFilter();
        long count = count(filter);
        Predicate predicate = null;
        if (null != filter) {
            predicate = convertFilter(entityClass, filter);
        }
        Sort sort = pageRequest.getSort();
        OrderSpecifier<?>[] orders = null;
        // 处理排序
        if (null != sort) {
            orders = new OrderSpecifier<?>[sort.size()];
            for (int i = 0; i < sort.size(); i++) {
                orders[i] = toOrder(root, builder, sort.get(i));
            }
        }

        List<Entity> list = repository.findAll(predicate, pageRequest.getOffset(),
                pageRequest.getSize(), orders);
        return new Page<VO>(BeanConvertUtils.convertDomainToVO(list,
                voClass), pageRequest, count);
    }

    @Override
    @Transactional
    public void updateAll(VO vo, Filter filter) {
        Assert.notNull(vo, "update vo is required.");
        checkHasUniqueProperty(vo);
        Predicate predicate = convertFilter(entityClass, filter);
        repository.updateAll(
                BeanConvertUtils.convertVOToDomain(vo, entityClass), predicate);

        if (hasCache && null != cacheService) {
            clearCachedDatas(this.getModule().getFullName());
        }

    }

    private void checkHasUniqueProperty(VO vo) {
        List<Unique> uniqueList = getUniques();
        if (!uniqueList.isEmpty()) {
            try {
                for (Unique uni : uniqueList) {
                    boolean hasUnique = true;
                    String[] propertys = uni.value();
                    List<String> uniquePropertys = new ArrayList<>();
                    for (String property : propertys) {
                        if (null == FieldUtils.readDeclaredField(vo, property,
                                true)) {
                            hasUnique = false;
                        } else {
                            uniquePropertys.add(property);
                        }
                    }
                    if (hasUnique) {
                        throw new RequestNotAcceptableException(
                                Messages.getMessage("persistence.unique.NotSupported", StringUtils.join(uniquePropertys, ",")));
                    }
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                throw new ServerErrorException(Messages.getMessage("persistence.property.read.error"),
                        e);
            }
        }
    }

    @Override
    @Transactional
    public void deleteAll(Filter filter) {
        Predicate predicate = null;
        if (null != filter) {
            predicate = convertFilter(entityClass, filter);
        }
        repository.deleteAll(predicate);

        if (hasCache && null != cacheService) {
            clearCachedDatas(this.getModule().getFullName());
        }
    }

    @Override
    @Transactional
    public void updateAll(VO vo, VO example) {
        Assert.notNull(vo, "update vo is required.");
        checkHasUniqueProperty(vo);
        Predicate predicate = convertExample(entityClass,
                BeanConvertUtils.convertVOToDomain(example, entityClass));
        repository.updateAll(
                BeanConvertUtils.convertVOToDomain(vo, entityClass), predicate);

        if (hasCache && null != cacheService) {
            clearCachedDatas(this.getModule().getFullName());
        }

    }

    @Override
    @Transactional
    public void deleteAll(VO example) {
        Predicate predicate = null;
        if (null != example) {
            predicate = convertExample(entityClass,
                    BeanConvertUtils.convertVOToDomain(example, entityClass));
        }
        repository.deleteAll(predicate);

        if (hasCache && null != cacheService) {
            clearCachedDatas(this.getModule().getFullName());
        }
    }

    <T> Predicate convertExample(Class<T> entityClass, IdEntity<String> entity) {
        RelationalPath<T> relationalPath = entityPathResolver
                .createPath(entityClass);
        PathBuilder<T> pathBuilder = entityPathResolver
                .getPathBuilder(relationalPath);
        Specification<T> specification = DynamicSpecifications.byExample(
                entity, entityClass);
        Predicate predicate = specification.toPredicate(relationalPath,
                pathBuilder, entityPathResolver);
        return predicate;
    }

    <T> Predicate convertFilter(Class<T> entityClass, Filter filter) {
        RelationalPath<T> relationalPath = entityPathResolver
                .createPath(entityClass);
        PathBuilder<T> pathBuilder = entityPathResolver
                .getPathBuilder(relationalPath);
        Specification<T> specification = DynamicSpecifications.bySearchFilter(
                filter, entityClass);
        Predicate predicate = specification.toPredicate(relationalPath,
                pathBuilder, entityPathResolver);
        return predicate;
    }

    <T> Predicate convertCriteria(Class<T> entityClass, Criteria criteria) {
        RelationalPath<T> relationalPath = entityPathResolver
                .createPath(entityClass);
        PathBuilder<T> pathBuilder = entityPathResolver
                .getPathBuilder(relationalPath);
        Specification<T> specification = DynamicSpecifications.bySearchCriteria(
                criteria, entityClass);
        Predicate predicate = specification.toPredicate(relationalPath,
                pathBuilder, entityPathResolver);
        return predicate;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    <T> OrderSpecifier toOrder(RelationalPath<T> root, PathBuilder<T> builder,
                               Order order) {

        Path<T> property = entityPathResolver.getProperty(root,
                order.getProperty());
        if (null == property) {
            property = (Path<T>) builder.get(order.getProperty());
        }

        return new OrderSpecifier(
                Direction.ASC.equals(order.getDirection()) ? com.mysema.query.types.Order.ASC
                        : com.mysema.query.types.Order.DESC, property);
    }

    void clearCachedDatas(String module) {
        long size = cacheService.size(module);
        for (int i = 0; i < size; i++) {
            cacheService.delete((String) cacheService.get(module, i));
        }

        cacheService.delete(module);
    }

    void putToClearDatas(String module, String key) {
        if (!cacheService.contains(module, key)) {
            cacheService.add(module, key);
        }
    }
}
